Skip to content

feat: implement ExitPlanMode HITL for Tool Permission Model#1589

Open
quay-devel wants to merge 4 commits into
ambient-code:mainfrom
quay-devel:feat/exit-plan-mode-hitl
Open

feat: implement ExitPlanMode HITL for Tool Permission Model#1589
quay-devel wants to merge 4 commits into
ambient-code:mainfrom
quay-devel:feat/exit-plan-mode-hitl

Conversation

@quay-devel
Copy link
Copy Markdown
Contributor

@quay-devel quay-devel commented May 14, 2026

Summary

Implements the Tool Permission Model spec from PR #1586, adding ExitPlanMode as a HITL (human-in-the-loop) tool that halts the event stream and waits for user approval — the same mechanism already used by AskUserQuestion.

  • Runner: ExitPlanMode added to BUILTIN_FRONTEND_TOOLS halt set; plan file content injected into tool args; Tier 1 allowlist completed with all missing tools
  • Backend: isAskUserQuestionToolCallisHITLToolCall to detect both tools for status derivation and snapshot compaction; new test cases for ExitPlanMode
  • Frontend: HITL detection generalized via shared hitl-tools.ts; new ExitPlanModeMessage component with approve/reject/request-changes actions

Closes #1583
Spec: #1586

Test plan

  • Backend Go tests pass (go test ./websocket/...)
  • Frontend build passes with 0 errors, 0 warnings (npm run build)
  • Frontend tests pass (668 passed, 12 skipped via npx vitest run)
  • Runner adapter syntax verified
  • No panic() in Go code, no any types in TypeScript
  • Manual: verify ExitPlanMode halts stream and shows plan approval UI
  • Manual: verify approve/reject/request-changes sends correct tool result

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Plan Review interface: users can now approve, reject plans, or request changes with feedback.
    • Expanded human-in-the-loop tool support to cover additional interaction scenarios.
  • Improvements

    • Enhanced tool detection consistency across the application for more reliable interaction management.

quay-devel and others added 2 commits May 14, 2026 18:41
Add ExitPlanMode as a human-in-the-loop tool alongside AskUserQuestion,
enabling plan approval workflows in ACP sessions. This implements the
spec from PR ambient-code#1586 (closes ambient-code#1583).

Runner:
- Add ExitPlanMode to BUILTIN_FRONTEND_TOOLS halt set
- Enrich ExitPlanMode tool args with plan file content from .claude/plans/
- Complete Tier 1 tool allowlist (NotebookEdit, WebFetch, TodoWrite, etc.)

Backend:
- Generalize isAskUserQuestionToolCall → isHITLToolCall to detect both
  AskUserQuestion and ExitPlanMode for status derivation and compaction
- Add ExitPlanMode test cases for waiting_input detection

Frontend:
- Generalize HITL detection in use-agent-status and stream-message
- Add ExitPlanModeMessage component with approve/reject/request-changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract shared hitl-tools.ts with normalizeToolName, isHITLTool,
  isAskUserQuestionTool, isExitPlanModeTool, and hasToolResult helpers
- Remove duplicated hasResult and tool detection functions from
  ask-user-question.tsx, exit-plan-mode.tsx, stream-message.tsx,
  and use-agent-status.ts
- Add 100KB size guard to _read_plan_file to prevent oversized events
- Log JSON errors during ExitPlanMode plan enrichment instead of
  silently swallowing them
- Use stable composite key for allowedPrompts list rendering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented May 14, 2026

Deploy Preview for cheerful-kitten-f556a0 canceled.

Name Link
🔨 Latest commit d7352bb
🔍 Latest deploy log https://app.netlify.com/projects/cheerful-kitten-f556a0/deploys/6a061e70c18ac30008a3cf94

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Warning

Rate limit exceeded

@quay-devel has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 30 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0f0d226d-67e0-49e2-a4f3-1a0f2a0bcab7

📥 Commits

Reviewing files that changed from the base of the PR and between 45eb7fc and d7352bb.

📒 Files selected for processing (2)
  • components/frontend/src/components/session/exit-plan-mode.tsx
  • components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
📝 Walkthrough

Walkthrough

Adds unified HITL (Human-in-the-Loop) tool infrastructure across backend, frontend, and runner to support both AskUserQuestion and the new ExitPlanMode tool. Backend now detects both tools to track waiting_input state; frontend shares tool detection helpers, renders a new plan-review UI component for ExitPlanMode, and updates status logic; runner adds ExitPlanMode to builtin tools, reads plan files, and expands the allowlist.

Changes

HITL Tool Support

Layer / File(s) Summary
Tool detection helpers
components/frontend/src/lib/hitl-tools.ts
New normalizeToolName, isAskUserQuestionTool, isExitPlanModeTool, isHITLTool, and hasToolResult helpers that normalize tool names (lowercase, alphabetic-only) and detect both HITL tool types. Shared across backend and frontend logic.
Backend HITL detection and state tracking
components/backend/websocket/agui_proxy.go, components/backend/websocket/agui_store.go, components/backend/websocket/agui_store_test.go
isHITLToolCall generalizes tool detection for both AskUserQuestion and ExitPlanMode. DeriveAgentStatus and compactFinishedRun now use HITL detection to preserve and infer waiting_input state. Tests expanded to verify both tool types are recognized in all letter casings.
Frontend HITL status and ask-user-question refactor
components/frontend/src/hooks/use-agent-status.ts, components/frontend/src/components/session/ask-user-question.tsx
useAgentStatus updated to scan for most recent unanswered HITL tool call (not just AskUserQuestion). AskUserQuestion component refactored to use centralized hasToolResult helper instead of local implementation.
ExitPlanMode UI component
components/frontend/src/components/session/exit-plan-mode.tsx
New ExitPlanModeMessage component renders a "Plan Review" card with formatted timestamp, markdown plan content, requested permissions list, and approve/reject/request-changes actions with optional feedback input. Submission state is tracked; actions are disabled until answered or submission completes.
Frontend message router and tool detection
components/frontend/src/components/ui/stream-message.tsx
Adds tool detection imports and a new conditional branch to render ExitPlanModeMessage when a tool use is identified as ExitPlanMode.
Runner ExitPlanMode support
components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
Adds ExitPlanMode to BUILTIN_FRONTEND_TOOLS. When Claude invokes ExitPlanMode, the adapter reads the most recent .claude/plans/*.md file (truncated to 100 KB, UTF-8) and injects its contents into the tool call's JSON arguments as planContent before emitting to the frontend.
Tool allowlist expansion
components/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py
DEFAULT_ALLOWED_TOOLS expanded to include ExitPlanMode, EnterPlanMode, NotebookEdit, WebFetch, TodoWrite, TaskOutput, TaskStop, EnterWorktree, ExitWorktree, CronCreate, CronDelete, CronList, and ScheduleWakeup, removing the permission prompt barrier for these tools.

Sequence Diagram

sequenceDiagram
    participant Claude as Claude (AI)
    participant Adapter as Adapter
    participant Backend as Backend<br/>(WebSocket)
    participant Frontend as Frontend
    participant User as User

    Claude->>Adapter: Invoke ExitPlanMode<br/>(tool call)
    Adapter->>Adapter: Read latest<br/>.claude/plans/*.md
    Adapter->>Adapter: Inject planContent<br/>into arguments
    Adapter->>Backend: Emit TOOL_CALL_START<br/>(ExitPlanMode)
    Backend->>Backend: isHITLToolCall()<br/>matches ExitPlanMode
    Backend->>Backend: DeriveAgentStatus()<br/>→ waiting_input
    Backend->>Frontend: Stream event:<br/>waiting_input state
    Frontend->>Frontend: useAgentStatus()<br/>detects HITL tool
    Frontend->>Frontend: stream-message checks<br/>isExitPlanModeTool()
    Frontend->>Frontend: Render ExitPlanModeMessage<br/>with plan content
    Frontend->>User: Display "Plan Review" UI<br/>(approve/reject/changes)
    User->>Frontend: Click approve/reject<br/>+ optional feedback
    Frontend->>Backend: Submit tool result<br/>via onSubmitAnswer
    Backend->>Backend: Snapshot TOOL_CALL_START<br/>to preserve waiting_input
    Backend->>Claude: Resume with result
    Claude->>Claude: Continue execution<br/>with user decision
Loading

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Security And Secret Handling ❌ Error Plan file content embedded in tool args is persisted in MESSAGES_SNAPSHOT events without redaction. May expose sensitive code details in logs and event streams. Redact plan content from MESSAGES_SNAPSHOT or implement access controls. Audit data sensitivity of .claude/plans/ files. Consider encryption for plan data in transit/at-rest.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title follows Conventional Commits format (feat: description) and clearly summarizes the main change: implementing ExitPlanMode as a HITL tool.
Linked Issues check ✅ Passed PR fully addresses issue #1583: ExitPlanMode now included in allowlist [mcp.py], integrated as HITL tool in backend/frontend matching AskUserQuestion, includes plan file injection [adapter.py], and enables approval workflow via frontend component.
Out of Scope Changes check ✅ Passed All changes directly support ExitPlanMode HITL implementation. Backend generalizes HITL detection, frontend adds approval UI, runner injects plan content and expands allowlist—all aligned with PR objectives.
Performance And Algorithmic Complexity ✅ Passed No blocking performance regressions found. String normalization efficient; message scanning has early break; plan file I/O called once per tool; React components acceptable.
Kubernetes Resource Safety ✅ Passed PR contains only application code (Go, TypeScript, Python). No Kubernetes manifests, resources, RBAC, or pod security policies present. Check not applicable.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/frontend/src/components/session/exit-plan-mode.tsx`:
- Around line 35-36: The variables planContent and allowedPrompts currently use
TypeScript assertions only; add runtime guards so planContent is set to
input.planContent if typeof input.planContent === "string" otherwise "" and set
allowedPrompts to Array.isArray(input.allowedPrompts) ? input.allowedPrompts as
AllowedPrompt[] : [] (or validate each element) before using ReactMarkdown and
.map; update the initialization of planContent and allowedPrompts in
exit-plan-mode.tsx to perform these checks so ReactMarkdown always gets a string
and .map runs on a real array.

In `@components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py`:
- Around line 105-108: The truncation checks byte length but slices by character
count, which can exceed _PLAN_FILE_MAX_BYTES for multi-byte UTF-8 chars; fix by
performing the truncation in bytes: read the file text into content, encode to
bytes (e.g., content_bytes = content.encode("utf-8")), if len(content_bytes) >
_PLAN_FILE_MAX_BYTES then slice the bytes to _PLAN_FILE_MAX_BYTES, decode back
to a string with a safe error handler (e.g., errors="ignore" or "replace") and
append the "\n\n[truncated]" marker before returning; update the logic around
plan_files, content, and _PLAN_FILE_MAX_BYTES accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 767b3627-8743-45b6-a69e-396e31eb5da9

📥 Commits

Reviewing files that changed from the base of the PR and between 63545c7 and 45eb7fc.

📒 Files selected for processing (10)
  • components/backend/websocket/agui_proxy.go
  • components/backend/websocket/agui_store.go
  • components/backend/websocket/agui_store_test.go
  • components/frontend/src/components/session/ask-user-question.tsx
  • components/frontend/src/components/session/exit-plan-mode.tsx
  • components/frontend/src/components/ui/stream-message.tsx
  • components/frontend/src/hooks/use-agent-status.ts
  • components/frontend/src/lib/hitl-tools.ts
  • components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
  • components/runners/ambient-runner/ambient_runner/bridges/claude/mcp.py

Comment thread components/frontend/src/components/session/exit-plan-mode.tsx Outdated
Comment thread components/runners/ambient-runner/ag_ui_claude_sdk/adapter.py
- Add runtime type guards for planContent (typeof string) and
  allowedPrompts (Array.isArray) in ExitPlanModeMessage to prevent
  runtime errors from unexpected backend data
- Fix byte-accurate truncation in _read_plan_file: slice encoded bytes
  instead of character count to respect the 100KB limit for multi-byte
  UTF-8 content

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: ExitPlanMode hangs indefinitely in ACP sessions — runner lacks plan approval handler

1 participant